package render;

import java.awt.Color;
import java.util.ArrayList;

import javax.media.j3d.Appearance;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Material;
import javax.media.j3d.Node;
import javax.media.j3d.PolygonAttributes;
import javax.media.j3d.Shape3D;
import javax.vecmath.Color3f;
import javax.vecmath.Matrix3d;
import javax.vecmath.Matrix4d;
import javax.vecmath.Point3d;
import javax.vecmath.Point3f;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;

import kernel.Fiber;

import com.sun.j3d.utils.geometry.GeometryInfo;
import com.sun.j3d.utils.geometry.NormalGenerator;

public class Tube {
	
	private Point3d[] crossSection;
	
	public Tube()
	{
		
	}

	public BranchGroup createTube(Fiber fiber, double tubeRadius, int steps, boolean isClosed)
	{
//		BranchGroup tubeBg = new BranchGroup();
		
		
		ArrayList<double[]> points = fiber.getPoints();
		int vertexSize = steps * 3 * points.size();
		
		double[] vertexList = new double[vertexSize];
		float[] normalList = new float[vertexSize];
		int[] indexList = new int[vertexSize * 2];
		
		
		Point3d[] path = new Point3d[points.size()];
		for(int i = 0; i < points.size(); i++)
		{
			double[] p = points.get(i);
			path[i] = new Point3d(p[0], p[1],p[2]);
		}
		
		
		createTubeGeometry(path, tubeRadius, steps, isClosed, vertexList, normalList, indexList);
		
		return createTubeGeometry(fiber, vertexList, indexList, normalList, fiber.getFiberColor());
	}
	
	private BranchGroup createTubeGeometry(Fiber fiber, double[] vertexPoints, int[] coordIndices, float[] normalList, Color color)
	{
		BranchGroup fiberPoints = new BranchGroup();
		NodeCharacteristics.setBranchGroupCapabilities(fiberPoints);
		
		Point3f[] vertices = new Point3f[vertexPoints.length/3];
		int verticesIndex = 0;
		for(int i = 0; i < vertexPoints.length; i+=3)
		{
			vertices[verticesIndex] = new Point3f((float)vertexPoints[i], (float)vertexPoints[i+1], (float)vertexPoints[i+2]);
			verticesIndex++;
		}
		
		Vector3f[] normals = new Vector3f[normalList.length/3];
		int normalsIndex = 0;
		for(int i = 0; i < normalList.length; i+=3)
		{
			normals[normalsIndex] = new Vector3f(normalList[i], normalList[i+1], normalList[i+2]);
			normalsIndex++;
		}
		
		GeometryInfo gi = new GeometryInfo( GeometryInfo.TRIANGLE_ARRAY );
		gi.setCoordinates( vertices );
		gi.setCoordinateIndices( coordIndices);

		NormalGenerator normalGenerator = new NormalGenerator();
		normalGenerator.generateNormals( gi );
		
		Appearance app = new Appearance();
		Material mat = new Material();
		mat.setDiffuseColor(new Color3f(color));
		app.setMaterial(mat);
		PolygonAttributes polAtt = new PolygonAttributes();
		polAtt.setPolygonMode(PolygonAttributes.POLYGON_FILL);
		app.setPolygonAttributes(polAtt);
		
		Shape3D tubeShape3d;
		try {
			tubeShape3d = new Shape3D(gi.getGeometryArray(), app);
			NodeCharacteristics.setShape3DCapabilites(tubeShape3d);
			tubeShape3d.setUserData(fiber);
			fiberPoints.addChild(tubeShape3d);
		} catch (Exception e) {
			System.err.println("not rendering fiber");
		}
		
		
		
		return fiberPoints;
	}

	/**
     * Creates the surface mesh for a tube with the indicated radius, traveling
     * along the indicated path.
     *
     * Adapted from idx3d code developed by Peter Walser,
     * http://www2.active.ch/~proxima
     *
     * @param path       points of the path that the tube is to run along.
     *                   The path will actually run along the center of the tube.
     * @param tubeRadius the radius of the tube itself
     * @param steps      number of triangles to make in the mesh
     * @param isClosed   indicates if the path is a closed loop or not
     * @param vertexList list of unique vertices
     * @param normalList list of normals corresponding to the vertex list
     * @param indexList  list of indices into the vertex list (and the other
     *                   lists, as well) that has an entry per triangle vertex.
     */
    private void createTubeGeometry( Point3d[] path,
                                       double tubeRadius,
                                       int steps,
                                       boolean isClosed,
                                       double[] vertexList,
                                       float[] normalList,
                                       int[] indexList ) {


        crossSection = createCrossSection( tubeRadius, steps );

        /*
         * Creation of all objects that will be used in the upcoming "for."
         * This could be split up to reduce the size of this method, but
         * there should be consideration for performance if the split
         * requires the creation of a significant number of new objects.
         * In the following implementation, no new objects are created within
         * the "for."
         */
        Vector3d forward = new Vector3d();          // vector pointing forward
        Vector3d up = new Vector3d();               // vector pointing up
        Vector3d right = new Vector3d();            // vector pointing right
        Matrix4d transformMatrix = new Matrix4d();  // transformation matrix
        Point3d tempVertex = new Point3d();         // transformed vertex
        Matrix3d rotationMatrix = new Matrix3d();   // rot. matrix for normals
        Point3d tempNormal = new Point3d();         // transformed normal
        Vector3f normalVector = new Vector3f();     // current normal vector

        int numSegments = path.length - 1;
        int vertexIndex = 0;
        for (int currPoint = 0; currPoint < path.length; currPoint++) {

            // Calculate Frenet frame matrix.
            
            // Get the path between this point and the next.
            if (currPoint < (path.length - 1)) {
                forward.sub( path[ currPoint + 1 ], path[ currPoint ] );
            } else {

                /*
                 * If the path is a closed loop, point towards the head vertex.
                 * Otherwise, the point towards the vertex.
                 */
                if (isClosed) {
                    forward.sub( path[ 0 ], path[ currPoint ] );
                } else {
                    forward.sub( path[ currPoint ], path[ currPoint - 1 ] );
                }
            }

            /* 
             * Create a vector to help create a coordinate frame using the 
             * forward vector as one axis.
             */
            forward.normalize();
            up.set( 0, 0, 1.0 );
            right.cross( forward, up );
            
            /*
             * If the up vector is parallel with the forward vector, choose
             * another vector or we won't be able to create a coordinate frame.
             */
            if (right.equals( new Vector3d( 0, 0, 0 ))) {
                up.set( -1.0, 0, 0 );
                right.cross( forward, up );
            }
            right.normalize();
            up.cross( forward, right );
            up.normalize();

            transformMatrix.setIdentity(); // reinit the matrix.
            transformMatrix.setColumn( 0, right.x,
                                          right.y,
                                          right.z,
                                          0d );
            transformMatrix.setColumn( 1, up.x,
                                          up.y,
                                          up.z,
                                          0d );
            transformMatrix.setColumn( 2, forward.x,
                                          forward.y,
                                          forward.z,
                                          0d );
            transformMatrix.setColumn( 3, path[ currPoint ].x,
                                          path[ currPoint ].y,
                                          path[ currPoint ].z,
                                          1d );

            // Add vertex nodes
            for (int k = 0; k < steps; k++) {

                // Transform the vertices of the original cross-section.
                transformMatrix.transform( crossSection[ k ], tempVertex );

                // Create the normal for this vertex.
                transformMatrix.get( rotationMatrix );
                rotationMatrix.transform( crossSection[ k ], tempNormal );
                normalVector.set( tempNormal );
                normalVector.normalize();
                vertexList[ vertexIndex ]   = tempVertex.x;
                normalList[ vertexIndex++ ] = normalVector.x;
                vertexList[ vertexIndex ]   = tempVertex.y;
                normalList[ vertexIndex++ ] = normalVector.y;
                vertexList[ vertexIndex ]   = tempVertex.z;
                normalList[ vertexIndex++ ] = normalVector.z;

            } // for (int k = 0; k < steps; k++)
        } // for (int currPoint = 0;...

        createIndexList( steps, numSegments, isClosed, indexList );
    }

    /**
     * Creates evenly spaced points along the surface of a cross-section of
     * the tube.
     * This particular version creates a circle cross-section.  Subclasses
     * should override this method to create cross-sections of different
     * shapes.
     *
     * Adapted from idx3d code developed by Peter Walser,
     * http://www2.active.ch/~proxima
     *
     * @param tubeRadius the radius of the tube itself.
     * @param steps number of triangles to make in the mesh.
     *
     * @return points along the surface of the cross-section.
     */
    private Point3d[] createCrossSection( double tubeRadius,
                                            int steps ) {

        /*
         * If this is a new cross-section, find equidistant points on the
         * circle that will serve to define the tube's cross-section.
         * Otherwise, if this is not a new cross-section, return the current
         * one.
         * We will be creating a new cross-section if there hasn't been one
         * before (null), or if the number of steps are the same and the radius
         * is the same (note that the origin of the circle is at (0, 0, 0).
         * Note: the points are ordered so that they proceed in a 
         * counter-clockwise progression.  This may be important for determining
         * normals and other surface vectors.
         */
        if ((crossSection == null) ||
            ((crossSection.length != steps) &&
             (crossSection[ 0 ].distance( new Point3d() ) != tubeRadius))) {

            crossSection = new Point3d[ steps ];
            double angle;
            for (int i = 0; i < steps; i++) {

                angle = (2 * Math.PI) * ((double) i / (double) steps);
                crossSection[ i ] = new Point3d( tubeRadius * Math.cos( angle ),
                                                 tubeRadius * Math.sin( angle ),
                                                 0 );
            } // for
        } // if ((crossSection == null) || ...

        return crossSection;
    }

    /**
     * Creates the list of indices into the vertex and other lists.
     * 
     * The crazy way the indexes are set is actually very organized.  The
     * calculations for the index numbers are done so that the groups
     * of three vertices (the triangles) are non-overlapping, adjacaent, and
     * proceed around the outside of the tube in a counter-clockwise fashion.
     * This is provided that the points for each cross-section described in 
     * the vertex list are sequentially ordered, going around the circle in a
     * counter-clockwise fashion (see createCrossSection()).  
     *
     * Adapted from idx3d code developed by Peter Walser,
     * http://www2.active.ch/~proxima
     *
     * @param steps       number of points that the cross-section is split into.
     * @param numSegments number of segments in the tube's path.
     *                    This is equivalent to one less than the number of
     *                    points in the path.
     * @param isClosed    indicates if the path is a closed loop or not
     * @param indexList   list of indices for the triangles of the mesh.
     *                    The indices are grouped in triplets, each triplet
     *                    representing one triangle (of course).
     */
    private void createIndexList( int steps,
                                    int numSegments,
                                    boolean isClosed,
                                    int[] indexList ) {

        // Create the vertex index list.
        int vertexA;
        int vertexB;
        int listIndex = 0;
        for (int i = 0; i < numSegments; i++) {

            for (int k = 0; k < (steps - 1); k++) {

                vertexA = (i * steps) + k;
                vertexB = vertexA + 1;
                listIndex = setIndexList( vertexA,
                                          vertexB,
                                          steps,
                                          indexList, 
                                          listIndex );

            } // for (int k = 0; k < (steps - 1); k++)

            vertexA = ((i + 1) * steps) - 1;
            vertexB = vertexA + 1 - steps;
            listIndex = setIndexList( vertexA,
                                      vertexB,
                                      steps,
                                      indexList, 
                                      listIndex );

        } // for (int i = 0; i < numSegments; i++)

        // If the loop is closed, connect the end of the loop with the head.
        int lastGroupStartIndex = numSegments * steps;
        if (isClosed) {

            for (int n = 0; n < (steps - 1); n++) {

                vertexA = n;
                vertexB = vertexA + 1;
                listIndex = setIndexList( vertexA,
                                          vertexB,
                                          lastGroupStartIndex,
                                          indexList, 
                                          listIndex );

            } // for (int n = 0; n < (steps - 1); n++)

            vertexA = steps - 1;
            vertexB = 0;
            listIndex = setIndexList( vertexA,
                                      vertexB,
                                      lastGroupStartIndex,
                                      indexList, 
                                      listIndex );
        } // if (isClosed)
    }

    /**
     * Sets the indicated indices into the indexList.
     * This sets the vertices for two triangles.  The order of the vertices 
     * is counter-clockwise, in relation to the outward normal (at least it's
     * supposed to be!).
     * 
     * Adapted from idx3d code developed by Peter Walser,
     * http://www2.active.ch/~proxima
     *
     * @see #createIndexList()
     * 
     * @param vertexA     index into vertex list.  Indicates a point on the
     *                    edge of a cross-section of the tube.
     * @param vertexB     like vertexA, on the same cross-section.  This 
     *                    point must be found counter-clockwise from A, in 
     *                    relation to the outward normal.
     * @param nextSection offset to next cross-section's indices
     * @param indexList   list of indices for the triangles of the mesh.
     *                    The indices are grouped in triplets, each triplet
     *                    representing one triangle (of course).  The new
     *                    vertex indices are added to this list.
     * @param listIndex   index into indexList, pointing at next location to 
     *                    be filled
     * 
     * @return updated index into indexList, pointing at next location to be 
     *         filled
     */
    private int setIndexList( int vertexA,
                                int vertexB,
                                int nextSection,
                                int[] indexList,
                                int listIndex ) {

//        int vertexC = vertexA + nextSection;
//        int vertexD = vertexB + nextSection;
//
//        indexList[ listIndex++ ] = vertexA;
//        indexList[ listIndex++ ] = vertexC;
//        indexList[ listIndex++ ] = vertexB;
//
//        indexList[ listIndex++ ] = vertexB;
//        indexList[ listIndex++ ] = vertexC;
//        indexList[ listIndex++ ] = vertexD;
    	
    	int vertexC = vertexB + nextSection;
    	int vertexD = vertexA + nextSection;

    	indexList[ listIndex++ ] = vertexB;
    	indexList[ listIndex++ ] = vertexC;
    	indexList[ listIndex++ ] = vertexA;

    	indexList[ listIndex++ ] = vertexA;
    	indexList[ listIndex++ ] = vertexC;
    	indexList[ listIndex++ ] = vertexD;

        return (listIndex);
    }
}
